/*******************************************************************************
 * Copyright 2011 Mauro Luigi Drago (drago.luigi@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.bytenose.extrautils.collect;

import java.util.Iterator;
import java.util.NoSuchElementException;

import com.bytenose.extrautils.annotations.NonNullable;
import com.bytenose.extrautils.annotations.UtilityClass;
import com.bytenose.extrautils.functional.MutableTuple2;
import com.bytenose.extrautils.functional.MutableTuple3;
import com.bytenose.extrautils.functional.MutableTuple4;

/**
 * Defines zipping methods.
 * @author Mauro Luigi Drago
 *
 */
@UtilityClass
final class WeakZipIterators {
	
	/**
	 * Private constructor to avoid instantiation.
	 */
	private WeakZipIterators() {
	}
	
	/**
	 * Iterates stepwise over a set of iterators and zips elements 
	 * into tuples.
	 * The iterators may yield a different number of elements. 
	 * If this is the case, i.e., some elements miss, 
	 * the tuples yielded by the returned iterator have null values for the 
	 * missing elements. 
	 * The returned iterator is a non-materialized iterator yielding mutable tuples.
	 * To improve performance, always the same tuple object is return
	 * over subsequent invocations of the {@link Iterator#next()} with
	 * tuple values reflecting the currently scanned elements.
	 * This avoid creating a lot of unnecessary objects, but limits the 
	 * applicability of the iterator.
	 * If values must be cached across different invocations of the 
	 * {@link Iterator#next()} method, first materialize the iterator
	 * or the tuple.
	 * The returned iterator supports {@link Iterator#remove()} if the 
	 * iterators specified as the method arguments support that functionality.
	 * 
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param it1 the first iterator, cannot be null.
	 * @param it2 the second iterator, cannot be null.
	 * 
	 * @return the iterator over the zipped elements.
	 * 
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	static <T1,T2> Iterator<MutableTuple2<T1,T2>> zipWeak(
			@NonNullable final Iterator<T1> it1, 
			@NonNullable final Iterator<T2> it2) 
			throws IllegalArgumentException {
		if (it1 == null) throw new IllegalArgumentException("it1");
		if (it2 == null) throw new IllegalArgumentException("it2");
		
		return new Iterator<MutableTuple2<T1,T2>>() {
			private boolean hasNext1, hasNext2;
			
			private boolean hasNext;
			private boolean initialized;
			private MutableTuple2<T1,T2> sharedTuple;
			
			@Override
			public boolean hasNext() {
				if (!initialized) initialize();
				
				return hasNext;
			}
			
			private void initialize() {
				findNext();
				sharedTuple = MutableTuple2.create(null, null);
				initialized = true;
			}
			
			@Override
			public MutableTuple2<T1,T2> next() throws NoSuchElementException {
				if (!hasNext) throw new NoSuchElementException();
				
				sharedTuple.setValue1(hasNext1 ? it1.next() : null);
				sharedTuple.setValue2(hasNext2 ? it2.next() : null);
				findNext();
				return sharedTuple;
			}
			
			private void findNext() {
				hasNext1 = it1.hasNext();
				hasNext2 = it2.hasNext();
				hasNext = hasNext1 || hasNext2;
			}

			@Override
			public void remove() {
				if (!initialized) throw new IllegalStateException();
				
				if (sharedTuple.getValue1() != null) it1.remove();
				if (sharedTuple.getValue2() != null) it2.remove();
			}
		};
	}
	
	/**
	 * Iterates stepwise over a set of iterators and zips elements 
	 * into tuples.
	 * The iterators may yield a different number of elements. 
	 * If this is the case, i.e., some elements miss, 
	 * the tuples yielded by the returned iterator have null values for the 
	 * missing elements.
	 * The returned iterator is a non-materialized iterator yielding mutable tuples.
	 * To improve performance, always the same tuple object is return
	 * over subsequent invocations of the {@link Iterator#next()} with
	 * tuple values reflecting the currently scanned elements.
	 * This avoid creating a lot of unnecessary objects, but limits the 
	 * applicability of the iterator.
	 * If values must be cached across different invocations of the 
	 * {@link Iterator#next()} method, first materialize the iterator
	 * or the tuple.
	 * The returned iterator supports {@link Iterator#remove()} if the 
	 * iterators specified as the method arguments support that functionality.
	 * 
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param it1 the first iterator, cannot be null.
	 * @param it2 the second iterator, cannot be null.
	 * @param it3 the third iterator, cannot be null.
	 * 
	 * @return the iterator over the zipped elements.
	 * 
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	static <T1, T2, T3> Iterator<MutableTuple3<T1,T2,T3>> zipWeak(
			@NonNullable final Iterator<T1> it1, 
			@NonNullable final Iterator<T2> it2,
			@NonNullable final Iterator<T3> it3) 
			throws IllegalArgumentException {
		if (it1 == null) throw new IllegalArgumentException("it1");
		if (it2 == null) throw new IllegalArgumentException("it2");
		if (it3 == null) throw new IllegalArgumentException("it3");
		
		return new Iterator<MutableTuple3<T1,T2,T3>>() {
			private boolean hasNext1, hasNext2, hasNext3;
			
			private boolean hasNext;
			private boolean initialized;
			private MutableTuple3<T1,T2,T3> sharedTuple;
			
			@Override
			public boolean hasNext() {
				if (!initialized) initialize();
				
				return hasNext;
			}
			
			private void initialize() {
				findNext();
				sharedTuple = MutableTuple3.create(null, null, null);
				initialized = true;
			}
			
			@Override
			public MutableTuple3<T1,T2,T3> next() throws NoSuchElementException {
				if (!hasNext) throw new NoSuchElementException();
				
				sharedTuple.setValue1(hasNext1 ? it1.next() : null);
				sharedTuple.setValue2(hasNext2 ? it2.next() : null);
				sharedTuple.setValue3(hasNext3 ? it3.next() : null);
				findNext();
				return sharedTuple;
			}
			
			private void findNext() {
				hasNext1 = it1.hasNext();
				hasNext2 = it2.hasNext();
				hasNext3 = it3.hasNext();
				hasNext = hasNext1 || hasNext2 || hasNext3;
			}
			
			@Override
			public void remove() {
				if (!initialized) throw new IllegalStateException();
				
				if (sharedTuple.getValue1() != null) it1.remove();
				if (sharedTuple.getValue2() != null) it2.remove();
				if (sharedTuple.getValue3() != null) it3.remove();
			}
		};
	}
	
	/**
	 * Iterates stepwise over a set of iterators and zips elements 
	 * into tuples.
	 * The iterators may yield a different number of elements. 
	 * If this is the case, i.e., some elements miss, 
	 * the tuples yielded by the returned iterator have null values for the 
	 * missing elements.
	 * The returned iterator is a non-materialized iterator yielding mutable tuples.
	 * To improve performance, always the same tuple object is return
	 * over subsequent invocations of the {@link Iterator#next()} with
	 * tuple values reflecting the currently scanned elements.
	 * This avoid creating a lot of unnecessary objects, but limits the 
	 * applicability of the iterator.
	 * If values must be cached across different invocations of the 
	 * {@link Iterator#next()} method, first materialize the iterator
	 * or the tuple.
	 * The returned iterator supports {@link Iterator#remove()} if the 
	 * iterators specified as the method arguments support that functionality.
	 * 
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param <T4> the type of the fourth argument.
	 * @param it1 the first iterator, cannot be null.
	 * @param it2 the second iterator, cannot be null.
	 * @param it3 the third iterator, cannot be null.
	 * @param it4 the fourth iterator, cannot be null.
	 * 
	 * @return the iterator over the zipped elements.
	 * 
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	static <T1,T2,T3,T4> Iterator<MutableTuple4<T1,T2,T3,T4>> zipWeak(
			@NonNullable final Iterator<T1> it1, 
			@NonNullable final Iterator<T2> it2,
			@NonNullable final Iterator<T3> it3,
			@NonNullable final Iterator<T4> it4) 
			throws IllegalArgumentException {
		if (it1 == null) throw new IllegalArgumentException("it1");
		if (it2 == null) throw new IllegalArgumentException("it2");
		if (it3 == null) throw new IllegalArgumentException("it3");
		if (it4 == null) throw new IllegalArgumentException("it4");
		
		return new Iterator<MutableTuple4<T1,T2,T3,T4>>() {
			private boolean hasNext1, hasNext2, hasNext3, hasNext4;
			
			private boolean hasNext;
			private boolean initialized;
			private MutableTuple4<T1,T2,T3,T4> sharedTuple;
			
			@Override
			public boolean hasNext() {
				if (!initialized) initialize();
				
				return hasNext;
			}
			
			private void initialize() {
				findNext();
				sharedTuple = MutableTuple4.create(null, null, null,null);
				initialized = true;
			}
			
			@Override
			public MutableTuple4<T1,T2,T3,T4> next() throws NoSuchElementException {
				if (!hasNext) throw new NoSuchElementException();
				
				sharedTuple.setValue1(hasNext1 ? it1.next() : null);
				sharedTuple.setValue2(hasNext2 ? it2.next() : null);
				sharedTuple.setValue3(hasNext3 ? it3.next() : null);
				sharedTuple.setValue4(hasNext4 ? it4.next() : null);
				findNext();
				return sharedTuple;
			}
			
			private void findNext() {
				hasNext1 = it1.hasNext();
				hasNext2 = it2.hasNext();
				hasNext3 = it3.hasNext();
				hasNext4 = it4.hasNext();
				hasNext = hasNext1 || hasNext2 || hasNext3 || hasNext4;
			}
			
			@Override
			public void remove() {
				if (!initialized) throw new IllegalStateException();
				
				if (sharedTuple.getValue1() != null) it1.remove();
				if (sharedTuple.getValue2() != null) it2.remove();
				if (sharedTuple.getValue3() != null) it3.remove();
				if (sharedTuple.getValue4() != null) it4.remove();
			}
		};
	}
}
